import { isPair } from '../nodes/identity.ts'
import { Pair } from '../nodes/Pair.ts'
import { YAMLMap } from '../nodes/YAMLMap.ts'
import { YAMLSeq } from '../nodes/YAMLSeq.ts'
import type { FlowCollection, Token } from '../parse/cst.ts'
import type { Schema } from '../schema/Schema.ts'
import type { CollectionTag } from '../schema/types.ts'
import type { ComposeContext, ComposeNode } from './compose-node.ts'
import type { ComposeErrorHandler } from './composer.ts'
import { resolveEnd } from './resolve-end.ts'
import { resolveProps } from './resolve-props.ts'
import { containsNewline } from './util-contains-newline.ts'
import { mapIncludes } from './util-map-includes.ts'

const blockMsg = 'Block collections are not allowed within flow collections'
const isBlock = (token: Token | null | undefined) =>
  token && (token.type === 'block-map' || token.type === 'block-seq')

export function resolveFlowCollection(
  { composeNode, composeEmptyNode }: ComposeNode,
  ctx: ComposeContext,
  fc: FlowCollection,
  onError: ComposeErrorHandler,
  tag?: CollectionTag
) {
  const isMap = fc.start.source === '{'
  const fcName = isMap ? 'flow map' : 'flow sequence'
  const NodeClass = (tag?.nodeClass ?? (isMap ? YAMLMap : YAMLSeq)) as {
    new (schema: Schema): YAMLMap.Parsed | YAMLSeq.Parsed
  }
  const coll = new NodeClass(ctx.schema)
  coll.flow = true
  const atRoot = ctx.atRoot
  if (atRoot) ctx.atRoot = false
  if (ctx.atKey) ctx.atKey = false

  let offset = fc.offset + fc.start.source.length
  for (let i = 0; i < fc.items.length; ++i) {
    const collItem = fc.items[i]
    const { start, key, sep, value } = collItem

    const props = resolveProps(start, {
      flow: fcName,
      indicator: 'explicit-key-ind',
      next: key ?? sep?.[0],
      offset,
      onError,
      parentIndent: fc.indent,
      startOnNewline: false
    })
    if (!props.found) {
      if (!props.anchor && !props.tag && !sep && !value) {
        if (i === 0 && props.comma)
          onError(props.comma, 'UNEXPECTED_TOKEN', `Unexpected , in ${fcName}`)
        else if (i < fc.items.length - 1)
          onError(
            props.start,
            'UNEXPECTED_TOKEN',
            `Unexpected empty item in ${fcName}`
          )
        if (props.comment) {
          if (coll.comment) coll.comment += '\n' + props.comment
          else coll.comment = props.comment
        }
        offset = props.end
        continue
      }
      if (!isMap && ctx.options.strict && containsNewline(key))
        onError(
          key as Token, // checked by containsNewline()
          'MULTILINE_IMPLICIT_KEY',
          'Implicit keys of flow sequence pairs need to be on a single line'
        )
    }
    if (i === 0) {
      if (props.comma)
        onError(props.comma, 'UNEXPECTED_TOKEN', `Unexpected , in ${fcName}`)
    } else {
      if (!props.comma)
        onError(
          props.start,
          'MISSING_CHAR',
          `Missing , between ${fcName} items`
        )
      if (props.comment) {
        let prevItemComment = ''
        loop: for (const st of start) {
          switch (st.type) {
            case 'comma':
            case 'space':
              break
            case 'comment':
              prevItemComment = st.source.substring(1)
              break loop
            default:
              break loop
          }
        }
        if (prevItemComment) {
          let prev = coll.items[coll.items.length - 1]
          if (isPair(prev)) prev = prev.value ?? prev.key
          if (prev.comment) prev.comment += '\n' + prevItemComment
          else prev.comment = prevItemComment
          props.comment = props.comment.substring(prevItemComment.length + 1)
        }
      }
    }

    if (!isMap && !sep && !props.found) {
      // item is a value in a seq
      // → key & sep are empty, start does not include ? or :
      const valueNode = value
        ? composeNode(ctx, value, props, onError)
        : composeEmptyNode(ctx, props.end, sep, null, props, onError)
      ;(coll as YAMLSeq).items.push(valueNode)
      offset = valueNode.range[2]
      if (isBlock(value)) onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg)
    } else {
      // item is a key+value pair

      // key value
      ctx.atKey = true
      const keyStart = props.end
      const keyNode = key
        ? composeNode(ctx, key, props, onError)
        : composeEmptyNode(ctx, keyStart, start, null, props, onError)
      if (isBlock(key)) onError(keyNode.range, 'BLOCK_IN_FLOW', blockMsg)
      ctx.atKey = false

      // value properties
      const valueProps = resolveProps(sep ?? [], {
        flow: fcName,
        indicator: 'map-value-ind',
        next: value,
        offset: keyNode.range[2],
        onError,
        parentIndent: fc.indent,
        startOnNewline: false
      })

      if (valueProps.found) {
        if (!isMap && !props.found && ctx.options.strict) {
          if (sep)
            for (const st of sep) {
              if (st === valueProps.found) break
              if (st.type === 'newline') {
                onError(
                  st,
                  'MULTILINE_IMPLICIT_KEY',
                  'Implicit keys of flow sequence pairs need to be on a single line'
                )
                break
              }
            }
          if (props.start < valueProps.found.offset - 1024)
            onError(
              valueProps.found,
              'KEY_OVER_1024_CHARS',
              'The : indicator must be at most 1024 chars after the start of an implicit flow sequence key'
            )
        }
      } else if (value) {
        if ('source' in value && value.source && value.source[0] === ':')
          onError(value, 'MISSING_CHAR', `Missing space after : in ${fcName}`)
        else
          onError(
            valueProps.start,
            'MISSING_CHAR',
            `Missing , or : between ${fcName} items`
          )
      }

      // value value
      const valueNode = value
        ? composeNode(ctx, value, valueProps, onError)
        : valueProps.found
          ? composeEmptyNode(
              ctx,
              valueProps.end,
              sep,
              null,
              valueProps,
              onError
            )
          : null
      if (valueNode) {
        if (isBlock(value)) onError(valueNode.range, 'BLOCK_IN_FLOW', blockMsg)
      } else if (valueProps.comment) {
        if (keyNode.comment) keyNode.comment += '\n' + valueProps.comment
        else keyNode.comment = valueProps.comment
      }

      const pair = new Pair(keyNode, valueNode)
      if (ctx.options.keepSourceTokens) pair.srcToken = collItem
      if (isMap) {
        const map = coll as YAMLMap.Parsed
        if (mapIncludes(ctx, map.items, keyNode))
          onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique')
        map.items.push(pair)
      } else {
        const map = new YAMLMap(ctx.schema)
        map.flow = true
        map.items.push(pair)
        const endRange = (valueNode ?? keyNode).range
        map.range = [keyNode.range[0], endRange[1], endRange[2]]
        ;(coll as YAMLSeq).items.push(map)
      }
      offset = valueNode ? valueNode.range[2] : valueProps.end
    }
  }

  const expectedEnd = isMap ? '}' : ']'
  const [ce, ...ee] = fc.end
  let cePos = offset
  if (ce && ce.source === expectedEnd) cePos = ce.offset + ce.source.length
  else {
    const name = fcName[0].toUpperCase() + fcName.substring(1)
    const msg = atRoot
      ? `${name} must end with a ${expectedEnd}`
      : `${name} in block collection must be sufficiently indented and end with a ${expectedEnd}`
    onError(offset, atRoot ? 'MISSING_CHAR' : 'BAD_INDENT', msg)
    if (ce && ce.source.length !== 1) ee.unshift(ce)
  }
  if (ee.length > 0) {
    const end = resolveEnd(ee, cePos, ctx.options.strict, onError)
    if (end.comment) {
      if (coll.comment) coll.comment += '\n' + end.comment
      else coll.comment = end.comment
    }
    coll.range = [fc.offset, cePos, end.offset]
  } else {
    coll.range = [fc.offset, cePos, cePos]
  }

  return coll
}
